package com.linkallcloud.web.utils;

import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
import java.util.Iterator;
import java.util.Set;

import javax.servlet.http.HttpServletRequest;

import com.alibaba.fastjson2.JSON;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.core.ZSetOperations;

import com.linkallcloud.cache.redis.ITopic;
import com.linkallcloud.cache.redis.bean.MapBean;
import com.linkallcloud.cache.redis.bean.PubSubBean;
import com.linkallcloud.cache.redis.bean.ZSetBean;
import com.linkallcloud.cache.redis.box.RedisTokenBox;
import com.linkallcloud.core.dto.Trace;
import com.linkallcloud.core.lang.Mirror;
import com.linkallcloud.core.lang.Strings;
import com.linkallcloud.core.log.Log;
import com.linkallcloud.core.log.Logs;
import com.linkallcloud.core.pagination.Page;
import com.linkallcloud.core.query.rule.Equal;
import com.linkallcloud.core.query.rule.GreaterEqual;
import com.linkallcloud.core.query.rule.LessEqual;
import com.linkallcloud.core.vo.OnlineUser;
import com.linkallcloud.core.www.ISimpleUser;

public abstract class BaseOnlineHelper<U extends ISimpleUser> implements IOnlineHelper {
    protected static final Log log = Logs.get();

    protected Mirror<U> mirror;

    @Autowired
    private ZSetBean zSetBean;

    @Autowired
    private MapBean mapBean;

    @Autowired
    private PubSubBean pubSubBean;
    
    @Autowired
    private RedisTokenBox tokenBox;

    @SuppressWarnings("unchecked")
    public BaseOnlineHelper() {
        super();
        try {
            mirror = Mirror.me((Class<U>) Mirror.getTypeParams(getClass())[0]);
        } catch (Throwable e) {
            if (log.isWarnEnabled()) {
                log.warn("!!!Fail to get TypeParams for self!", e);
            }
        }
    }

    public Class<U> getSessionUserClass() {
        return (null == mirror) ? null : mirror.getType();
    }

    public String appOnlineCache() {
        return "USERS_ONLINE_" + getAppCode();
    }

    public String appLoginCache() {
        return "USERS_LOGIN_" + getAppCode();
    }

    // @PostConstruct
    public void initSubscribe() {
        pubSubBean.subscribe(new MessageListener() {
            @Override
            public void onMessage(Message message, byte[] bytes) {
                String loginName = new String(message.getBody());
                offline(loginName);
                log.info(getAppCode() + " ==> SSO offline msg:" + message);
            }
        }, ITopic.TOPIC_LOGOUT_APP);
    }

    @Override
    public String login(ISimpleUser su, Duration timeout) {
        String token = tokenBox.put(su, timeout);
        try {
            OnlineUser user = new OnlineUser(su.appCode(), su.getLoginName(), token, timeout);
            zSetBean.add(appOnlineCache(), user.getLoginName(), Instant.now().toEpochMilli());
            mapBean.hset(appLoginCache(), user.getLoginName(), JSON.toJSONString(user));
        } catch (Throwable e) {
            log.error(e.getMessage(), e);
        }
        return token;

    }

    @Override
    public void logout(String token) {
        if (!Strings.isBlank(token)) {
            U su = tokenBox.get(token, getSessionUserClass());
            if (su != null) {
                zSetBean.remove(appOnlineCache(), su.getLoginName());
                mapBean.hdel(appLoginCache(), su.getLoginName());

            }
            tokenBox.remove(token);
        }
    }

    @Override
    public void logout(HttpServletRequest request) {
        String token = Controllers.getLacToken(request);
        if (!Strings.isBlank(token)) {
            logout(token);
        }
    }

    @Override
    public void offline(String loginName) {
        zSetBean.remove(appOnlineCache(), loginName);

        OnlineUser ou = getUser(loginName);
        if (ou != null) {
            tokenBox.remove(ou.getToken());

            mapBean.hdel(appLoginCache(), loginName);
        }
    }

    @Override
    public ISimpleUser getSessionUser(HttpServletRequest request) {
        return Controllers.getSessionUser(request, tokenBox, getSessionUserClass());
    }

    @Override
    public OnlineUser getUser(String loginName) {
        String us = mapBean.hget(appLoginCache(), loginName);
        return JSON.parseObject(us, OnlineUser.class);
    }

    @Override
    public Long clear() {
        return zSetBean.clear(appOnlineCache(), 0,
                LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
    }

    @Override
    public Long count() {
        return zSetBean.size(appOnlineCache());
    }

    @Override
    public Long count(Date start, Date end) {
        double min = start != null ? start.getTime() : 0;
        double max = end != null ? end.getTime()
                : LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
        return zSetBean.size(appOnlineCache(), min, max);
    }

    @Override
    public Page<OnlineUser> getPage(Trace t, Page<OnlineUser> page) {
        page.checkPageParameters();

        if (page.hasRule4Field("loginName")) {
            return score(t, page);
        } else if (page.hasRule4Field("loginTimeGe") || page.hasRule4Field("loginTimeLe")) {
            return reverseRangeByScoreWithScores(t, page);
        } else {
            return reverseRangeWithScores(t, page);
        }
    }

    private Page<OnlineUser> reverseRangeByScoreWithScores(Trace t, Page<OnlineUser> page) {
        if (page.hasRule4Field("loginTimeGe") || page.hasRule4Field("loginTimeLe")) {
            Long geDt = getGeDate(page);
            Long leDt = getLeDate(page);
            double min = geDt != null ? geDt : 0;
            double max =
                    leDt != null ? leDt : LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();

            if (page.isSearchCount() || page.getRecordsTotal() <= 0) {
                long count = zSetBean.size(appOnlineCache(), min, max);
                page.setRecordsTotal(count);
                page.setRecordsFiltered(page.getRecordsTotal());
            }
            Set<ZSetOperations.TypedTuple<String>> us = zSetBean.reverseRangeByScoreWithScores(appOnlineCache(), min,
                    max, page.getStart(), page.getLength());
            if (us != null && !us.isEmpty()) {
                Iterator<ZSetOperations.TypedTuple<String>> itr = us.iterator();
                while (itr.hasNext()) {
                    ZSetOperations.TypedTuple<String> u = itr.next();
                    OnlineUser olu = new OnlineUser(u.getValue(), u.getScore());
                    page.addData(olu);
                }
            }
        }
        return page;
    }

    private Page<OnlineUser> reverseRangeWithScores(Trace t, Page<OnlineUser> page) {
        if (page.isSearchCount() || page.getRecordsTotal() <= 0) {
            long count = count();
            page.setRecordsTotal(count);
            page.setRecordsFiltered(page.getRecordsTotal());
        }
        Set<ZSetOperations.TypedTuple<String>> us = zSetBean.reverseRangeWithScores(appOnlineCache(), page.getStart(),
                page.getStart() + page.getLength() - 1);
        if (us != null && !us.isEmpty()) {
            Iterator<ZSetOperations.TypedTuple<String>> itr = us.iterator();
            while (itr.hasNext()) {
                ZSetOperations.TypedTuple<String> u = itr.next();
                OnlineUser olu = new OnlineUser(u.getValue(), u.getScore());
                page.addData(olu);
            }
        }
        return page;
    }

    private Page<OnlineUser> score(Trace t, Page<OnlineUser> page) {
        if (page.hasRule4Field("loginName")) {
            Equal rule = (Equal) page.getRule4Field("loginName");
            String loginName = (String) rule.getValue();
            if (!Strings.isBlank(loginName)) {
                Double dt = zSetBean.score(appOnlineCache(), loginName);
                if (dt != null && dt > 0) {
                    if (timeFitGe(t, page, dt) && timeFitLe(t, page, dt)) {
                        page.setRecordsTotal(1);
                        page.setRecordsFiltered(page.getRecordsTotal());
                        OnlineUser olu = new OnlineUser(loginName, dt);
                        page.addData(olu);
                    } else {
                        page.setRecordsTotal(0);
                        page.setRecordsFiltered(page.getRecordsTotal());
                    }
                } else {
                    page.setRecordsTotal(0);
                    page.setRecordsFiltered(page.getRecordsTotal());
                }
            }
        }
        return page;
    }

    private boolean timeFitGe(Trace t, Page<OnlineUser> page, Double loginTime) {
        Long geDt = getGeDate(page);
        if (geDt != null && loginTime < geDt) {
            return false;
        }
        return true;
    }

    private boolean timeFitLe(Trace t, Page<OnlineUser> page, Double loginTime) {
        Long leDt = getLeDate(page);
        if (leDt != null && loginTime > leDt) {
            return false;
        }
        return true;
    }

    private Long getGeDate(Page<OnlineUser> page) {
        if (page.hasRule4Field("loginTimeGe")) {
            GreaterEqual ge = (GreaterEqual) page.getRule4Field("loginTimeGe");
            Long geDt = (Long) ge.getValue();
            return geDt;
        }
        return null;
    }

    private Long getLeDate(Page<OnlineUser> page) {
        if (page.hasRule4Field("loginTimeLe")) {
            LessEqual ge = (LessEqual) page.getRule4Field("loginTimeLe");
            Long leDt = (Long) ge.getValue();
            return leDt;
        }
        return null;
    }
}
